<?php
/*
Copyright (c) 2007, Retrix Hosting, Inc.
http://retrix.com/
All rights reserved.

Based on Simple OpenID PHP Class by http://www.fivestores.com/

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.

Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.

Neither the name of Retrix Hosting nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

define('DEBUG', false);

define('OPENID_REQUEST_CURL', 100);
define('OPENID_REQUEST_SOCKET', 101);

define('OPENID_MODE_CANCEL', 200);

define('OPENID_ERROR_NO_SERVERS_FOUND', 400);
define('OPENID_ERROR_CURL', 401);
define('OPENID_ERROR_SOCKET', 402);
define('OPENID_ERROR_SOCKET_TIMEOUT', 403);
define('OPENID_ERROR_VALIDATION', 404);


//=======================================================================
// CLASS: OpenIDConsumer
//=======================================================================

class OpenIDConsumer
{
	var $openid_url;
	var $openid_server;
	var $openid_trust_root;
	var $openid_return_to_url;
	var $openid_approved_url;
	var $openid_policy_url;

	var $fields = array();
	var $error = array();
	

	//---------------------------------------------------------------------
	// CONSTRUCTOR: 
	//---------------------------------------------------------------------

	function OpenIDConsumer($openid_url = '', $requestType = OPENID_REQUEST_CURL)
	{
		if (DEBUG) { error_log('OpenIDConsumer::OpenIDConsumer ' . $openid_url); }

		$this->setOpenIdUrl($openid_url);

		$this->setRequiredFields(array('nickname','email','fullname'));
		$this->setOptionalFields(array('dob','gender','postcode','country','language','timezone'));

		if ($requestType == OPENID_REQUEST_CURL && !function_exists('curl_exec'))
		{
			die('Error: OpenIDConsumer requires curl extension to work');
		}
	}

	//---------------------------------------------------------------------
	// FUNCTION: setOpenIDUrl
	//---------------------------------------------------------------------

	function setOpenIDUrl($openid_url)
	{
		$this->openid_url = $this->normalizeUrl($openid_url);

		if (DEBUG) { error_log('OpenIDConsumer::setOpenIDUrl ' . $this->openid_url); }
	}


	//---------------------------------------------------------------------
	// FUNCTION: getOpenIDUrl
	//---------------------------------------------------------------------

	function getOpenIDUrl()
	{
		return $this->openid_url;
	}


	//---------------------------------------------------------------------
	// FUNCTION: setTrustRoot
	//---------------------------------------------------------------------
	
	// c'mon trust me, guy!

	function setTrustRoot($openid_trust_root)
	{
		$this->openid_trust_root = $this->normalizeUrl($openid_trust_root);

		if (DEBUG) { error_log('OpenIDConsumer::setTrustRoot ' . $this->openid_trust_root); }
	}


	//---------------------------------------------------------------------
	// FUNCTION: setReturnToUrl
	//---------------------------------------------------------------------

	// Send return_to response from OpenID server to this script

	function setReturnToUrl($openid_return_to_url)
	{
		$this->openid_return_to_url = $this->normalizeUrl($openid_return_to_url);

		if (DEBUG) { error_log('OpenIDConsumer::setReturnToUrl ' . $this->openid_return_to_url); }
	}


	//---------------------------------------------------------------------
	// FUNCTION: setApprovedURL
	//---------------------------------------------------------------------
	
	// Send approved response from OpenID server to this script

	function setApprovedURL($openid_approved_url)
	{
		$this->openid_approved_url = $this->normalizeUrl($openid_approved_url);

		if (DEBUG) { error_log('OpenIDConsumer::setApprovedURL ' . $this->openid_approved_url); }
	}


	//---------------------------------------------------------------------
	// FUNCTION: setOpenIDServer
	//---------------------------------------------------------------------
	
	// call getOpenIDServer to set this, or set it from cache, database, or session.
	
	function setOpenIDServer($openid_server)
	{
		$this->openid_server = $this->normalizeUrl($openid_server);

		if (DEBUG) { error_log('OpenIDConsumer::setOpenIDServer ' . $this->openid_server); }
	}


	//---------------------------------------------------------------------
	// FUNCTION: normalizeUrl
	//---------------------------------------------------------------------

    function normalizeUrl($url)
    {
        $parsedUrl = @parse_url(trim($url));

        if (!isset($parsedUrl['scheme']))
        {
        	if (!strstr($url, 'http'))
        	{
				$url = 'http://' . $url;
				
				$parsedUrl = @parse_url($url);
			}
        }

        if (!isset($parsedUrl['path']))
        {
            $url .= '/';
        }
        
        return $url;
    }


	//---------------------------------------------------------------------
	// FUNCTION: setPolicyUrl
	//---------------------------------------------------------------------
	
	// A URL which the Consumer provides to give the End User a place to
	// read about the how the profile data will be used. The Identity
	// Provider SHOULD display this URL to the End User if it is given.

	function setPolicyUrl($openid_policy_url)
	{
		$this->openid_policy_url = $this->normalizeUrl($openid_policy_url);

		if (DEBUG) { error_log('OpenIDConsumer::setPolicyUrl ' . $this->openid_policy_url); }
	}


	//---------------------------------------------------------------------
	// FUNCTION: setRequiredFields
	//---------------------------------------------------------------------
	
	// Comma-separated list of field names which, if absent from the
	// response, will prevent the Consumer from completing the registration
	// without End User interation. The field names are those that are
	// specified in the Response Format (Response Format), with the
	// "openid.sreg." prefix removed.
	
	function setRequiredFields($requiredFields)
	{
		$this->fields['required'] = $requiredFields;
	}


	//---------------------------------------------------------------------
	// FUNCTION: setOptionalFields
	//---------------------------------------------------------------------
	
	// Comma-separated list of field names Fields that will be used by
	// the Consumer, but whose absence will not prevent the registration
	// from completing. The field names are those that are specified in the
	// Response Format (Response Format), with the "openid.sreg." prefix removed.

	function setOptionalFields($optionalFields)
	{
		$this->fields['optional'] = $optionalFields;
	}


	//---------------------------------------------------------------------
	// FUNCTION: getError
	//---------------------------------------------------------------------

	function getError()
	{
		if (sizeof($this->error) > 0)
		{
			return array('code' => $this->error[0], 'description' => $this->error[1]);
		}
		else
		{
			return false;
		}
	}


	//---------------------------------------------------------------------
	// FUNCTION: setError
	//---------------------------------------------------------------------

	function setError($errorCode, $description = '')
	{
		if (DEBUG) { error_log('ERROR: errorCode: ' . $errorCode . ', description:' . $description); }

	   	$this->error = array($errorCode, $description);
	}


	//---------------------------------------------------------------------
	// FUNCTION: getOpenIDServer
	//---------------------------------------------------------------------
	
	// Get server url from identity page.
	// Returns false if server is not found
	
	function getOpenIDServer($requestType = OPENID_REQUEST_CURL)
	{
		if (!isset($this->openid_server))
		{
			$requestUrl = $this->openid_url;
	
			switch ($requestType)
			{
				case OPENID_REQUEST_CURL:
				{
					$response = $this->CURL_Request($requestUrl);
				}
				break;
	
				case OPENID_REQUEST_SOCKET:
				{
					$response = $this->FSOCK_Request($requestUrl);
				}
				break;
			}
	
			// get server details from response
			preg_match_all('/<link[^>]*rel="openid.server"[^>]*href="([^"]+)"[^>]*\/?>/i', $response, $matches1);
			preg_match_all('/<link[^>]*href="([^"]+)"[^>]*rel="openid.server"[^>]*\/?>/i', $response, $matches2);
			$servers = array_merge($matches1[1], $matches2[1]);
	
			// get delegate from response
			preg_match_all('/<link[^>]*rel="openid.delegate"[^>]*href="([^"]+)"[^>]*\/?>/i', $response, $matches1);
			preg_match_all('/<link[^>]*href="([^"]+)"[^>]*rel="openid.delegate"[^>]*\/?>/i', $response, $matches2);
			$delegates = array_merge($matches1[1], $matches2[1]);
			
			if (sizeof($servers) == 0)
			{
				$this->setError(OPENID_ERROR_NO_SERVERS_FOUND, 'Unable to find OpenID server for ' . $requestUrl);
				return false;
			}
	
			if (sizeof($delegates) > 0)
			{
				$this->setOpenIDUrl($delegates[0]);
	
				if (DEBUG) { error_log('OpenIDConsumer::getOpenIDServer (openid.delegate from ' . $requestUrl . ' is ' . $this->openid_url . ')'); }
			}
	
			$this->setOpenIDServer($servers[0]);
	
			if (DEBUG) { error_log('OpenIDConsumer::getOpenIDServer (openid.server from ' . $requestUrl . ' is ' . $this->openid_server . ')'); }
		}

			
		return $this->openid_server;
	}


	//---------------------------------------------------------------------
	// FUNCTION: CURL_Request
	//---------------------------------------------------------------------
	
	// Remember, SSL MUST BE SUPPORTED
	
	function CURL_Request($url, $method = 'GET', $params = '')
	{ 
		if (is_array($params))
		{
			$params = $this->array2url($params);
		}
		
		$curl = curl_init($url . ($method == 'GET' && $params != '' ? '?' . $params : ''));

		if (DEBUG) { error_log('OpenIDConsumer::CURL_Request ' . $url . ($method == 'GET' && $params != '' ? '?' . $params : '')); }

		curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
		curl_setopt($curl, CURLOPT_HEADER, false);
		curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
		curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($curl, CURLOPT_HTTPGET, ($method == "GET"));
		curl_setopt($curl, CURLOPT_POST, ($method == "POST"));

		if ($method == "POST")
		{
			curl_setopt($curl, CURLOPT_POSTFIELDS, $params);
		}
		
		$response = curl_exec($curl);
		
		if (curl_errno($curl) == 0)
		{
			$response;
		}
		else
		{
			$this->setError('OPENID_ERROR_CURL', curl_error($curl));
			return false;
		}

		return $response;
	}
	

	//---------------------------------------------------------------------
	// FUNCTION: FSOCK_Request
	//---------------------------------------------------------------------
	
	// todo: make this work!
	
	function FSOCK_Request($url, $method = 'GET', $params = '')
	{
		$fp = fsockopen("ssl://www.openidentity.ca", 443, $errno, $errstr, 3); // Connection timeout is 3 seconds

		if (!$fp) 
		{
			$this->setError(OPENID_ERROR_SOCKET, $errstr);
			return false;
		}
		else 
		{
			$request = $method . " /server HTTP/1.0\r\n";
			$request .= "User-Agent: Retrix Hosting OpenID (http://www.openidentity.ca/)\r\n";
			$request .= "Connection: close\r\n\r\n";
			fwrite($fp, $request);
			stream_set_timeout($fp, 4); // Connection response timeout is 4 seconds
			$res = fread($fp, 2000);
			$info = stream_get_meta_data($fp);
			fclose($fp);
		
			if ($info['timed_out']) 
			{
				$this->setError(OPENID_ERROR_SOCKET_TIMEOUT, 'Connection timed out.');
				return false;
			}
			else 
			{
				return $res;
			}
		}
	}


	//---------------------------------------------------------------------
	// FUNCTION: getApproval
	//---------------------------------------------------------------------
	
	// Redirect user to OpenId server for approval
	
	function getApproval($requestType = OPENID_REQUEST_CURL)
	{
		if (!$this->getOpenIDServer($requestType))
		{
			return false;
		}
		
		$params = array();
		$params['openid.mode'] = 'checkid_setup';
		$params['openid.ns'] = 'http://specs.openid.net/auth/2.0';
		$params['openid.identity'] = $this->openid_url;
		$params['openid.return_to'] = $this->openid_return_to_url;
		$params['openid.trust_root'] = $this->openid_trust_root;
		$params['openid.sreg.policy_url'] = $this->openid_policy_url;
		
		if (count($this->fields['required']) > 0)
		{
			$params['openid.sreg.required'] = implode(',', $this->fields['required']);
		}

		if (count($this->fields['optional']) > 0)
		{
			$params['openid.sreg.optional'] = implode(',', $this->fields['optional']);
		}

		$openIdServer = $this->openid_server . "?". $this->array2url($params);

		if (DEBUG) { error_log('OpenIDConsumer::getApproval openIdServer: ' . $openIdServer); }
		
		if (headers_sent())
		{
			// Use JavaScript to redirect if content has been previously sent (not recommended, but safe)
			echo '<script language="JavaScript" type="text/javascript">window.location=\'';
			echo $openIdServer;
			echo '\';</script>';
		}
		else
		{
			header('Location: ' . $openIdServer);
		}
	}
	

	//---------------------------------------------------------------------
	// FUNCTION: validateWithServer
	//---------------------------------------------------------------------
	
	function validateWithServer($requestType = OPENID_REQUEST_CURL)
	{
		if (DEBUG) { error_log('OpenIDConsumer::validateWithServer'); }
		
		if (DEBUG) { error_log('OpenIDConsumer::validateWithServer request: ' . var_export($_REQUEST, true)); }
		
		if ($_REQUEST['openid_mode'] == 'cancel')
		{
			$this->setError(OPENID_MODE_CANCEL, 'OpenID authentication cancelled.');
			return false;
		}
		else
		{
			$params = array(
				'openid.assoc_handle' => urlencode($_REQUEST['openid_assoc_handle']),
				'openid.signed' => urlencode($_REQUEST['openid_signed']),
				'openid.sig' => urlencode($_REQUEST['openid_sig'])
			);
			
			// Send only required parameters to confirm validity
			$signedArray = explode(",", str_replace('sreg.','sreg_', $_REQUEST['openid_signed']));
	
			for ($i=0; $i < count($signedArray); $i++)
			{
				$s = str_replace('sreg_','sreg.', $signedArray[$i]);
				$c = $_REQUEST['openid_' . $signedArray[$i]];
	
				$params['openid.' . $s] = urlencode($c);
			}
	
			$params['openid.mode'] = "check_authentication";
			$params['openid.ns'] = 'http://specs.openid.net/auth/2.0';
	
			if (DEBUG) { error_log('OpenIDConsumer::validateWithServer  params: ' . var_export($params, true)); }
	
			if (!isset($this->openid_server))
			{
				if (!$this->getOpenIDServer($requestType))
				{
					return false;
				}
			}
	
			$response = $this->CURL_Request($this->openid_server, 'GET', $params);
	
			if (DEBUG) { error_log('OpenIDConsumer::validateWithServer  data: ' . var_export($response, true)); }
	
			$responseData = $this->splitResponse($response);
	
			if ($responseData['is_valid'] == 'true') 
			{
				$requestData = array();
				
				$fields = array_merge($this->fields['required'], $this->fields['optional']);
				
				for ($i = 0; $i < sizeof($fields); $i++)
				{
					$field = $fields[$i];
					$requestData['openid_sreg_' . $field] = $_REQUEST['openid_sreg_' . $field];
				}
	
				return $requestData;
			}
			else
			{
				$this->setError(OPENID_ERROR_VALIDATION, 'Validation failed: ' . $responseData['error']);
				return false;
			}
		}
	}


	//---------------------------------------------------------------------
	// FUNCTION: array2url
	//---------------------------------------------------------------------
	
	// converts associated array to URL Query String

	function array2url($arr)
	{ 
		if (!is_array($arr))
		{
			return false;
		}

		foreach ($arr as $key => $value)
		{
			$query .= $key . "=" . $value . "&";
		}

		return $query;
	}


	//---------------------------------------------------------------------
	// FUNCTION: splitResponse
	//---------------------------------------------------------------------

	function splitResponse($response) 
	{
		$r = array();
		$response = explode("\n", $response);

		foreach ($response as $line) 
		{
			$line = trim($line);

			if ($line != "") 
			{
				list($key, $value) = explode(":", $line, 2);
				$r[trim($key)] = trim($value);
			}
		}

	 	return $r;
	}
}

?>